iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Modern Web

Vue 前端工程師視角看 Flutter Web系列 第 20

Day 20|Vite build 流程與最佳化策略回顧

  • 分享至 

  • xImage
  •  

模組五|建置與部署(Day 20–24)

先備:Day 1(兩個對照 demo 的來歷)、Day 11(SPA 的 rewrite 原罪,Day 23 會再撞一次)。本篇沒有 Dart code,是替模組五校正量尺的一天。

進入模組五之前,先招認一件尷尬的事:我經手的 Vue 專案,vite build 從開專案到上線,config 一行都沒改過。倒不是懶,Vite 的預設就是夠好,好到我們忘了它底下替我們做了多少事。

接下來四天我要拿 Flutter Web 的 build 產物來對照。秤不準自己的重量,就沒資格說別人重,所以今天先把 Vite 的 build 管線拆開,順便把四個之後每天都會冒出來的名詞——tree shaking、code splitting、AOT、deferred import——都在今天這篇講過定義。

結論先講:

Vite 的最佳化是一排你可以逐項打開的旋鈕,Flutter 的最佳化是編譯器替你做完的既成事實。

旋鈕多代表自由,也代表責任落在你身上;既成事實代表省事,也代表那就是你的天花板。這句話會在後面四天反覆兌現。

Vue 怎麼做

Vite 是兩套系統縫在一起的產物:dev 模式只用 esbuild 把 node_modules 依賴先併好(pre-bundling),你自己寫的 code 靠瀏覽器原生 ESM 直接載,完全不打包;vite build 則整包交給 Rollup 做完整打包。我們平常感受到的「快」大多來自前者,上線品質卻全部取決於後者——這也是很多人 dev 一切正常、build 完才出事的原因。

vite build 預設就替你做完的事,有四項請記住名字,因為模組五每天都會用到:

  • tree shaking(打包時掃掉沒被引用到的程式碼):Rollup 靜態分析 ESM 的 import/export,沒人用到的 export 不進 bundle。它吃得動靜態 ESM,吃不動 CommonJS 那種執行期才知道要載什麼的寫法——這是老套件拖垮 bundle 最常見的原因。
  • code splitting(把 bundle 切成多包、用到才載):只要你寫 import(),Rollup 就自動切出一個獨立 chunk。vue-router 的 route-level lazy loading 就是這件事的包裝。
  • 資產指紋:輸出檔名帶 content hash(index-BqX3xxxx.js),內容一變檔名就變。這是整套快取策略的地基,Day 23 會回收這個伏筆。
  • CSS 抽離與壓縮、esbuild minify(壓縮:去空白、改短變數名)、modulepreload 注入(讓瀏覽器提早抓後續會用到的模組),全部預設開啟。

講完預設,把我自己的設定攤開。下面這段是 flight-booking-vuevite.config.ts,不是節錄,是全檔:

// vite.config.ts(flight-booking-vue,全檔 7 行)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
})

七行,其中兩行 import、一行註解、一行空白。沒有 build 區塊、沒有 rollupOptions、沒有 manualChunks——這個帶著 vue-router、Pinia、Tailwind v4、六個執行時依賴與十個開發依賴的 app,最佳化設定是零。

我把它原樣留著,因為它就是模組五的前提:Day 22 那組數字是「零最佳化的 Vue」對上「零最佳化的 Flutter Web」,兩邊都沒開外掛,比起來才公平。

唯一被動過的一行不在 config 裡,在 package.json:8

"build": "vue-tsc -b && vite build"

這行的意思是:跑 vite build 之前,先讓 vue-tsc 把整個專案的型別檢查一遍,型別不過就不會產出 dist。之所以要多這一道,是因為 Vite 的轉譯器 esbuild 只轉譯、不檢查型別——它把 .ts 的型別標註剝掉就送走,型別錯誤一個都不會攔。Vue 專案裡的型別安全,是靠這條 && 手動接上去的——語言本身沒給你這道保險。這個細節請先記在心裡,等一下對照 Flutter 時會用到。

如果哪天真的需要壓體積,慣用的三板斧長這樣。這段是示範用的設定,我的 demo 沒有採用:

// vite.config.js —— 需要動手最佳化時的起手式
import { visualizer } from 'rollup-plugin-visualizer'

export default {
  plugins: [visualizer({ gzipSize: true })], // 板斧一:先量,不要瞎猜
  build: {
    target: 'es2020',
    rollupOptions: {
      output: {
        manualChunks: {
          // 板斧二:把不常變的依賴獨立成一包,讓瀏覽器快取活得久一點
          vendor: ['vue', 'vue-router', 'pinia'],
        },
      },
    },
  },
}

板斧三是路由層級的動態 import:() => import('./views/Report.vue')。首屏只載首屏要用的東西,其他頁面等使用者真的走過去再說。三板斧的共同點是——你隨時可以只開其中一個,也可以一個都不開,程式照跑。

Flutter Web 怎麼做

對照組先劇透一口:flutter build web,一個指令,沒有 config 檔可調。沒有 rollupOptions、沒有 manualChunks、沒有 plugin 生態,pubspec.yaml 裡也找不到任何等價的 build 區塊。

Dart 編譯器(dart2js 或 dart2wasm)做的是整個程式的 AOT 編譯(ahead-of-time:事先編譯成目標格式,不在瀏覽器裡才編)。這個差別比字面上大:Rollup 是把一堆模組黏起來再刪掉沒用到的,編譯器則是把整個程式當一個封閉世界看待,連 Material icons 字型都會自動 subset——--tree-shake-icons 預設開啟,你用了哪幾個 icon 就只留哪幾個字符。

它也順手做掉了我剛剛在 Vue 端手動接上的那件事。Dart 沒有「只轉譯不檢查」這個檔位,型別錯就是編譯不過,flutter build web 天生就是一道型別閘門,不需要誰在指令列上加 &&

唯一接近 code splitting 的原語是 deferred import(Dart 的「先別載這包,等我叫你」):在 import 後面掛 deferred as 別名,那個 library 就不會進首包,要用之前先 await 一次 loadLibrary()。它跟 Vite 的 import() 目的相同,代價完全不同——Vite 那邊你什麼都不用做,寫了 import() 就自動切;Dart 這邊你得手動宣告、手動載入、手動處理載入中的畫面。完整的可跑範例排在 Day 22,因為要看到它的效果,得先看到體積數字。

想插手打包過程?基本上沒門,編譯器說了算。

差異與坑

坑一:別去找不存在的旋鈕。 我一開始反射性地想找「Flutter 版的 rollupOptions」,翻文件翻了一晚上,最後結論是它不存在。Flutter 的哲學是編譯器比你懂最佳化——以 AOT 語言來說大致成立,但也代表 bundle 過重時,你手上的槓桿遠比 Vite 少。症狀:你在 flutter build web --help 裡上上下下找了十分鐘,想找一個對應 manualChunks 的參數,然後發現整份說明只有二十幾個 flag,沒有一個跟切 chunk 有關。

坑二:tree shaking 的作用層不同。 Vite 在模組層做,範圍是「你 import 進來的東西」;Dart 在語言層做,範圍是「整個程式可觸及的 code」,理論上更徹底。但兩邊都有一個共同的盲點——它們都刪不掉引擎本體,而 Flutter Web 的引擎本體正好是體積的大頭。症狀:你把 app 的 code 刪到只剩一個 Text('hi') 重新 build,產物只小了幾十 KB,因為佔位子的從頭到尾都不是你寫的那些。

坑三:code splitting 的顆粒度差一個等級。 Vite 是「寫了 import() 就自動切」,Flutter 是「手動宣告 deferred、手動 await、切完還是切不動引擎」。同一個名詞,一邊是免費的預設行為,一邊是要規劃的架構決策。症狀:你把某個大頁面改成 deferred import,滿懷期待重測 Lighthouse,首載數字幾乎沒動——省下來的那幾十 KB 淹沒在 MB 級的引擎裡了。

坑四:型別閘門的位置,決定了 agent 能跑多遠。 Vue 端我得自己在 build script 寫 vue-tsc -b && 才有型別檢查,拿掉它,一個型別錯到離譜的分支照樣 build 成功、照樣部署上線;Flutter 端這道閘門焊死在編譯器裡,拆不掉。這對 agent 工作流的差別很直接——同樣是讓 agent 大改一輪,Dart 那邊「build 過了」約等於「型別上沒亂寫」,JS 那邊「build 過了」只代表「語法沒錯」。症狀:agent 改完 Vue 專案回報「build 成功」,你上線後才發現某個 prop 型別根本對不上,而 CI 從頭到尾一片綠。

小結與下一篇

一句話:Vite 給你一整面調音台,Flutter 只給你少數幾個開關——先把調音台上自己專案的基準值量清楚,明天才有得比。

而我的基準值誠實得有點難堪:七行 config、零最佳化。Day 21 進正題,flutter build web 到底編出了什麼?CanvasKit、skwasm,以及一個已經死掉的 HTML renderer——順便招認我在 renderer 這題上同樣什麼都沒調。

參考資料

如果你卡在語法

深入原理


上一篇
Day 19|狀態管理選型結論:什麼情境選什麼
下一篇
Day 21|Flutter Web build 流程:CanvasKit 與 renderer 選項
系列文
Vue 前端工程師視角看 Flutter Web21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言